組込み現場の「C++」プログラミング 明日から使える徹底入門

高木 信尚(株式会社クローバーフィールド

3.5 初期化と終了処理

ここでは,主にプログラムの開始時と終了時に何が行われるのかについて解説することにします.スタートアップの記述やマルチタスク動作にもかかわる内容ですので,組込み開発ではC++の初期化と終了処理について正確に理解しておく必要があります.

3.5.1 静的初期化と動的初期化

Cの場合,静的記憶域期間を持つオブジェクトは,スタートアップで2種類の初期化が行われます.すなわち,初期値を持たないオブジェクトはゼロクリアされ,初期値を持つオブジェクトはROMからRAMに初期値がコピーされます.C++でも,まずはこれと同じことが行われます.このような単純な初期化のことを「静的初期化」といいます.

次に,明示的なコンストラクタを持つクラスのオブジェクトや,定数式以外の初期化子を持つオブジェクトの初期化を行う必要があります.これを「動的初期化」といいます.

class A
{
public:
    A(int arg)
    {
         …
    }
};
A a(123); // ← 明示的なコンストラクタを持つクラスの場合 
double root2 = std::sqrt(2.0); // ← 定数式以外の初期化子を持つ場合 

「動的初期化」は,同じ翻訳単位の中では,必ず上に記述したものから下に記述したものの順に行われます.上記のコードであれば,aの初期化のほうがroot2の初期化より先に行われることになります.

それぞれの翻訳単位の初期化を行うルーチンは,リンカによって集められ,初期化リストが作られます.そして,何らかの方法で,この初期化リストに登録されているルーチンをプログラムの起動時にすべて実行してやらなければなりません(図3.5参照).

●図3.5 リンカが各翻訳単位の初期化ルーチンを集める翻訳単位

図3.5 リンカが各翻訳単位の初期化ルーチンを集める翻訳単位

初期化ルーチンを呼び出す具体的な方法は完全に処理系に依存するので,詳しくは処理系のマニュアルを読んでください.

なお,初期化リストに登録されたルーチンがどの順番に呼び出されるかは不定です.通常は,リンクした順序で呼び出されることになるでしょう.しかし,翻訳単位間での初期化の順序は,一般的にはコントロールできないと考えてください.

動的初期化は,プログラムの開始時に,静的初期化に続いて行われます.Cではmain関数が必ず最初に実行されますが,C++ではmain関数より先に,動的初期化によってmain以外の関数が呼び出される点に注意してください.なお,OSの支援を受けない自立処理系(フリースタンディング環境の処理系)の場合,main関数があるとはかぎりませんが,main関数に相当するプログラム開始位置より先に動的初期化が行われることになります.μITRONのような静的にリンクする形態のOSを使用する場合には,OSの起動より前に動的初期化が行われることもあります.

3.5.2 局所的な静的記憶域期間を持つ オブジェクトの初期化

「局所的な静的記憶域期間を持つオブジェクト」と書くと何のことかわからないかもしれませんが,要するに,関数の中でstaticを付けて定義したオブジェクトのことです.こうしたオブジェクトの場合も,明示的なコンストラクタを持つクラスのオブジェクトであったり,定数式以外の初期化子を持つオブジェクトの場合には,動的な初期化が行われます.しかし,関数の外で定義した非局所オブジェクトの初期化とはタイミングが異なります.

局所的な静的記憶域期間を持つオブジェクトの初期化は,そのオブジェクトの定義位置に,実行パスが最初に差しかかったときに初めて行われます.

class A
{
public:
    A(int arg)
    {
         …
    }
};
void func()
{
    static A a(123); // ← ここに実行パスが最初に差しかかったときに初めて初期化される 
     …
}

そのため,もしオブジェクトの定義位置に実行パスが差しかからなければ,そのオブジェクトは初期化されることがありません.また,非局所オブジェクトの動的初期化中に,局所的な静的記憶域期間を持つオブジェクトの定義を含む関数が呼び出され,そのオブジェクトの定義位置に実行パスが差しかかれば,その時点で初期化が行われます.

上記のコードを擬似コードを使って表すと,次のようになります.

void func()
{
    static bool __initialized = false;
    static char __a[sizeof(A)]; // ← 注意! 境界調整に対する配慮は行っていない 
    if (!__initialized)
    {
        __initialized = true;
        new(__a) A(123);
    }
    A& a = *(A*)__a;
     …
}

問題はここからです.局所的な静的記憶域期間を持つオブジェクトはただ一度だけ行われなければなりません.しかし,マルチタスク環境では,適切に排他制御しなければ,同時に2つ以上のタスクから初期化を行ってしまうことがあります.

ところが,上記の擬似コードで表したような処理はコンパイラが勝手に挿入するため,外からは原則としてどうすることもできません.処理系によっては,適切に排他制御を行うための関数呼び出しを,たとえば,次のように挿入します.

void func()
{
    static bool __initialized = false;
    static char __a[sizeof(A)]; // ← 注意! 境界調整に対する配慮は行っていない 
    lock();
    if (!__initialized)
    {        __initialized = true;
        new(__a) A(123);
    }
    unlock();
    A& a = *(A*)__a;
     …
}

そして,ユーザーはlockおよびunlock関数を定義すれば,局所的な静的記憶域期間を持つオブジェクトの初期化を排他制御することができます.しかし,処理系がこうした対応を行ってくれない場合には,アプリケーションでどうにかしなければなりません.

3.5.3 終了処理

組込みプログラムの場合,終了ということがそもそも起こらないことも少なくありませんが,C++の言語仕様としては終了についても規定があります.プログラムの終了時には,静的記憶域期間を持つオブジェクトに明示的なデストラクタがある場合,それらを実行することになります.多くの場合,初期化ルーチンと同様の方法で,リンカが終了処理ルーチンを集めて終了処理リストを作り,何らかの方法でそのリストに登録されたルーチンを呼び出すことになります.

原則として,同じ翻訳単位の中で定義されたオブジェクトについては,初期化とは逆順で終了処理が呼び出されます.しかし,厳密には終了処理の呼び出し順序はもっとルールが複雑です.もっとも,言語規格のとおりの順序で終了処理を呼び出す処理系は,どちらかといえば少数派です.そのような処理系では,atexitと同じ仕組みを用いて終了処理ルーチンを登録します.そのため,リンカが終了処理ルーチンを集めるといったことは行わないようです.